/*
 * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.querydsl.collections;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Operator;
import com.querydsl.core.types.Ops;
import com.querydsl.core.util.MathUtils;
import com.querydsl.core.util.ReflectionUtils;

/**
 * {@code CollQueryFunctions} defines function implementation for use in ColQueryTemplates
 *
 * @author tiwe
 *
 */
public final class CollQueryFunctions {

    private interface BinaryFunction {

        Number apply(Number num1, Number num2);
    }

    private static final BinaryFunction SUM = new BinaryFunction() {
        @Override
        public Number apply(Number num1, Number num2) {
            return MathUtils.sum(num1, num2);
        }
    };

    private static final BinaryFunction MAX = new BinaryFunction() {
        @Override
        public Number apply(Number num1, Number num2) {
            if (num1.getClass().equals(num2.getClass()) && num1 instanceof Comparable) {
                @SuppressWarnings("unchecked") // The types are interchangeable, guarded by previous check
                Comparable<Number> left = (Comparable<Number>) num1;
                return left.compareTo(num2) < 0 ? num2 : num1;
            } else {
                BigDecimal n1 = new BigDecimal(num1.toString());
                BigDecimal n2 = new BigDecimal(num2.toString());
                return n1.compareTo(n2) < 0 ? num2 : num1;
            }
        }
    };

    private static final BinaryFunction MIN = new BinaryFunction() {
        @Override
        public Number apply(Number num1, Number num2) {
            if (num1.getClass().equals(num2.getClass()) && num1 instanceof Comparable) {
                @SuppressWarnings("unchecked") // The types are interchangeable, guarded by previous check
                Comparable<Number> left = (Comparable<Number>) num1;
                return left.compareTo(num2) < 0 ? num1 : num2;
            } else {
                BigDecimal n1 = new BigDecimal(num1.toString());
                BigDecimal n2 = new BigDecimal(num2.toString());
                return n1.compareTo(n2) < 0 ? num1 : num2;
            }
        }
    };

    private static final List<Object> nullList = Arrays.asList((Object) null);

    public static boolean equals(Object o1, Object o2) {
        return Objects.equal(o1, o2);
    }

    public static <T extends Comparable<? super T>> int compareTo(T c1, T c2) {
        if (c1 == null) {
            return c2 == null ? 0 : -1;
        } else if (c2 == null) {
            return 1;
        } else {
            return c1.compareTo(c2);
        }
    }

    public static <A extends Comparable<? super A>> boolean between(A a, A b, A c) {
        return compareTo(a, b) >= 0 && compareTo(a, c) <= 0;
    }

    public static double cot(double x) {
        return Math.cos(x) / Math.sin(x);
    }

    public static double coth(double x) {
        return Math.cosh(x) / Math.sinh(x);
    }

    public static double degrees(double x) {
        return x * 180.0 / Math.PI;
    }

    public static double radians(double x) {
        return x * Math.PI / 180.0;
    }

    public static double log(double x, int y) {
        return Math.log(x) / Math.log(y);
    }

    @Nullable
    public static <T> T coalesce(T... args) {
        for (T arg : args) {
            if (arg != null) {
                return arg;
            }
        }
        return null;
    }

    public static <T> T nullif(T first, T second) {
        if (first.equals(second)) {
            return null;
        } else {
            return first;
        }
    }

    public static int getYearMonth(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.YEAR) * 100 + cal.get(Calendar.MONTH) + 1;
    }

    public static int getDayOfMonth(Date date) {
        return getField(date, Calendar.DAY_OF_MONTH);
    }

    public static int getDayOfWeek(Date date) {
        return getField(date, Calendar.DAY_OF_WEEK);
    }

    public static int getDayOfYear(Date date) {
        return getField(date, Calendar.DAY_OF_YEAR);
    }

    private static int getField(Date date, int field) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(field);
    }

    public static int getHour(Date date) {
        return getField(date, Calendar.HOUR_OF_DAY);
    }

    public static int getMilliSecond(Date date) {
        return getField(date, Calendar.MILLISECOND);
    }

    public static int getMinute(Date date) {
        return getField(date, Calendar.MINUTE);
    }

    public static int getMonth(Date date) {
        return getField(date, Calendar.MONTH) + 1;
    }

    public static int getSecond(Date date) {
        return getField(date, Calendar.SECOND);
    }

    public static int getWeek(Date date) {
        return getField(date, Calendar.WEEK_OF_YEAR);
    }

    public static int getYear(Date date) {
        return getField(date, Calendar.YEAR);
    }

    public static int getYearWeek(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.YEAR) * 100 + cal.get(Calendar.WEEK_OF_YEAR);
    }

    public static <T> Collection<T> leftJoin(Collection<T> coll) {
        if (coll == null || coll.isEmpty()) {
            @SuppressWarnings("unchecked") // List only contains null
            Collection<T> rv = (Collection<T>) nullList;
            return rv;
        } else {
            return coll;
        }
    }

    private static Number reduce(Iterable<Number> source, BinaryFunction f) {
        Iterator<Number> it = source.iterator();
        Number result = it.next();
        while (it.hasNext()) {
            result = f.apply(result, it.next());
        }
        return result;
    }

    public static Number aggregate(Collection<Number> source, Expression<?> expr, Operator aggregator) {
        @SuppressWarnings("unchecked") // This is a number expression
        Class<Number> numberType = (Class<Number>) expr.getType();
        if (aggregator == Ops.AggOps.AVG_AGG) {
            Number sum = reduce(source, SUM);
            return sum.doubleValue() / source.size();
        } else if (aggregator == Ops.AggOps.COUNT_AGG) {
            return (long) source.size();
        } else if (aggregator == Ops.AggOps.COUNT_DISTINCT_AGG) {
            if (!Set.class.isInstance(source)) {
                source = Sets.newHashSet(source);
            }
            return (long) source.size();
        } else if (aggregator == Ops.AggOps.MAX_AGG) {
            return MathUtils.cast(reduce(source, MAX), numberType);
        } else if (aggregator == Ops.AggOps.MIN_AGG) {
            return MathUtils.cast(reduce(source, MIN), numberType);
        } else if (aggregator == Ops.AggOps.SUM_AGG) {
            return MathUtils.cast(reduce(source, SUM), numberType);
        } else {
            throw new IllegalArgumentException("Unknown operator " + aggregator);
        }
    }

    public static boolean like(final String str, String like) {
        final StringBuilder pattern = new StringBuilder(like.length() + 4);
        for (int i = 0; i < like.length(); i++) {
            final char ch = like.charAt(i);
            if (ch == '%') {
                pattern.append(".*");
                continue;
            } else if (ch == '_') {
                pattern.append('.');
                continue;
            } else if (ch == '.' || ch == '$' || ch == '^') {
                pattern.append('\\');
            }
            pattern.append(ch);
        }
        if (pattern.toString().equals(like)) {
            return str.equals(like);
        } else {
            return str.matches(pattern.toString());
        }
    }

    public static boolean like(String str, String like, char escape) {
        return like(str, like);
    }

    public static boolean likeIgnoreCase(String str, String like) {
        final StringBuilder pattern = new StringBuilder(like.length() + 4);
        for (int i = 0; i < like.length(); i++) {
            final char ch = like.charAt(i);
            if (ch == '%') {
                pattern.append(".*");
                continue;
            } else if (ch == '_') {
                pattern.append('.');
                continue;
            } else if (ch == '.' || ch == '$' || ch == '^') {
                pattern.append('\\');
            }
            pattern.append(ch);
        }
        if (pattern.toString().equals(like)) {
            return str.equalsIgnoreCase(like);
        } else {
            return Pattern.compile(pattern.toString(), Pattern.CASE_INSENSITIVE)
                    .matcher(str).matches();
        }
    }

    public static boolean likeIgnoreCase(String str, String like, char escape) {
        return likeIgnoreCase(str, like);
    }

    public static <T> T get(Object parent, String f) {
        try {
            Field field = ReflectionUtils.getFieldOrNull(parent.getClass(), f);
            if (field != null) {
                field.setAccessible(true);
                @SuppressWarnings("unchecked")
                T rv = (T) field.get(parent);
                return rv;
            } else {
                throw new IllegalArgumentException("No field " + f + " for " + parent.getClass());
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private CollQueryFunctions() { }


}
